using System;
using System.Collections.Generic;
using System.Linq;
using CQRS.Events.Memento;
using MongoDB.Driver;
using MongoDB.Driver.Builders;

namespace CQRS.Events
{
    public class EventRepository<TEvent> : IEventRepository<TEvent> where TEvent : Event
    {
        private readonly MongoCollection<TEvent> _eventMongoCollection;
        private readonly MongoCollection<EventProviderDto> _eventProviderMongoCollection;
        private readonly MongoDatabase _mongoDatabase;
        private readonly MongoCollection<Snapshot> _snapshotMongoCollection;

        public EventRepository(IMongoDatabaseHelper mongoDatabaseHelper)
        {
            _mongoDatabase = mongoDatabaseHelper.GetMongoDatabase();
            _eventProviderMongoCollection = _mongoDatabase.GetCollection<EventProviderDto>("EventProviders");
            _eventMongoCollection = _mongoDatabase.GetCollection<TEvent>("Events");
            _snapshotMongoCollection = _mongoDatabase.GetCollection<Snapshot>("Snapshots");
        }

        #region IEventRepository<TEvent> Members

        public IEnumerable<TEvent> GetAllEvents(Guid eventProviderId)
        {
            IMongoQuery query = Query.EQ("EventProviderId", eventProviderId);
            return _eventMongoCollection.Find(query);
        }

        public IEnumerable<TEvent> GetEventsSinceLastSnapshot(Guid eventProviderId)
        {
            IMongoQuery query = GetSnapshotMongoQuery(eventProviderId);

            return _eventMongoCollection.Find(query);
        }

        public long GetEventCountSinceLastSnapshot(Guid eventProviderId)
        {
            IMongoQuery query = GetSnapshotMongoQuery(eventProviderId);

            return _eventMongoCollection.Find(query).Count();
        }

        public void Save(IEventProvider<TEvent> eventProvider)
        {
            int version = GetEventProviderVersion(eventProvider);

            if (version != eventProvider.Version)
                throw new ConcurrentEventSaveViolationException();

            foreach (Event domainEvent in eventProvider.GetChanges())
            {
                SaveEvent(domainEvent, eventProvider);
            }

            eventProvider.UpdateVersion(eventProvider.Version + eventProvider.GetChanges().Count());
            UpdateEventProviderVersion(eventProvider);
        }

        public ISnapshot GetSnapshot(Guid eventProviderId)
        {
            IMongoQuery query = Query.And(Query.EQ("EventProviderId", eventProviderId), Query.NE("Version", "-1"));
            return _snapshotMongoCollection.Find(query).FirstOrDefault();
        }

        public void SaveShapshot(IEventProvider<TEvent> entity)
        {
            StoreSnapshot(new Snapshot(entity.EventProviderId, entity.Version, ((IOriginator) entity).CreateMemento()));
        }

        #endregion

        private IMongoQuery GetSnapshotMongoQuery(Guid eventProviderId)
        {
            ISnapshot snapshot = GetSnapshot(eventProviderId);

            int snapshotVersion = snapshot != null
                                      ? snapshot.Version
                                      : -1;

            IMongoQuery query = Query.And(Query.EQ("EventProviderId", eventProviderId),
                                          Query.GT("Version", snapshotVersion));
            return query;
        }

        private void SaveEvent(Event domainEvent, IEventProvider<TEvent> eventProvider)
        {
            domainEvent.AggregateId = eventProvider.Id;
            _eventMongoCollection.Insert(domainEvent, SafeMode.True);
            var originator = eventProvider as IOriginator;
            if (originator != null)
            {
                var snapshot = new Snapshot(eventProvider.EventProviderId, eventProvider.Version,
                             originator.CreateMemento());
                //remove the previous snapshots of this event provider since they do not add value any more.
                _snapshotMongoCollection.Remove(Query.EQ("EventProviderId", eventProvider.Id));
                _snapshotMongoCollection.Insert(snapshot, SafeMode.True);
            }
        }

        private void StoreSnapshot(ISnapshot snapshot)
        {
            _snapshotMongoCollection.Insert(snapshot, SafeMode.True);
        }

        private void UpdateEventProviderVersion(IEventProvider<TEvent> eventProvider)
        {
            var eventProviderDto = new EventProviderDto(eventProvider.Id, eventProvider.Version);
            _eventProviderMongoCollection.Insert(eventProviderDto, SafeMode.True);
        }

        private int GetEventProviderVersion(IEventProvider<TEvent> eventProvider)
        {
            IMongoQuery query = Query.EQ("EventProviderId", eventProvider.Id);
            EventProviderDto provider = _eventProviderMongoCollection.Find(query).FirstOrDefault();
            return provider == null ? 0 : provider.Version;
        }
    }
}